﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using DiscreteEventModel;
using DiscreteEventModel.Common;
using GenericLibrary.ExtensionMethods;
using System.Diagnostics;

namespace SpikingNeuralNetwork {

    using Model;

    //The efficacy of a synapse should degrade on each spike passing through it; And will only be compensated if some of the post-synapse neurons responded with causaltive spike firing.
    //The neuron should be less sensitive to the future inputs if its previous outputs were not responded by post-synapse neurons.
    public partial class Neuron : NeuralNetwork.NeuralAgent {

        public virtual double PostFiringPotential { get { return -1; } }
        public virtual double FiringThreshold { get { return 1; } }
        public virtual double FiringLatency { get { return 0.1; } }
        public virtual double SynapticPlasticity { get { return 0.01; } }
        public double MembranePotential { get; private set; }
        private Dictionary<int, double> OutputSynapticWeights { get; set; }

        public Neuron(DiscreteEvent<NeuralNetwork.NeuralAgent, NeuralNetwork>.Runtime runtime)
            : base(runtime) {
            this.OutputSynapticWeights = new Dictionary<int, double>();
        }

        public Neuron(DiscreteEvent<NeuralNetwork.NeuralAgent, NeuralNetwork>.Runtime runtime, int id)
            : base(runtime, id) { }

        public override void Serialize(IBinaryWriter writer) {
            base.Serialize(writer);
            writer.Write(this.OutputSynapticWeights.Count);
            foreach(KeyValuePair<int, double> keyValue in this.OutputSynapticWeights) {
                writer.Write(keyValue.Key);
                writer.Write(keyValue.Value);
            }
        }

        public override void Deserialize(IBinaryReader reader) {
            base.Deserialize(reader);
            this.OutputSynapticWeights = new Dictionary<int, double>();
            for(int i = reader.ReadInt32(); i > 0; i--) {
                this.OutputSynapticWeights[reader.ReadInt32()] = reader.ReadDouble();
            }
        }

        /// <summary>
        /// The membrane potential will decay exponentially.
        /// dm = -a*exp(-(-ln(m/a)))*dt = -m*dt
        /// </summary>
        /// <param name="timeStep"></param>
        protected override void OnSynchronize(double timeStep) {
            this.MembranePotential -= this.MembranePotential * timeStep;
        }

        protected override DiscreteTime OnOutput(DiscreteEvent<NeuralNetwork.NeuralAgent, NeuralNetwork>.Runtime runtime) {
            #region Strengthen synaptic weight
            foreach(Spike tracedSpike in runtime.GetEventTrace<Spike>()) {
                double synapticWeight;
                if(tracedSpike.SourceNeuron.OutputSynapticWeights.TryGetValue(this.ID, out synapticWeight)) {
                    tracedSpike.SourceNeuron.OutputSynapticWeights[this.ID] = tracedSpike.StrengthenSynapticWeight(this, synapticWeight);
                }
            }
            #endregion

            #region Propagate spikes
            new Spike(runtime, 0, (this.FiringThreshold - this.PostFiringPotential) / this.OutputSynapticWeights.Count, this);
            #endregion

            #region Update the mambrane potential after firing
            this.MembranePotential = this.PostFiringPotential;
            #endregion

            return DiscreteTime.Infinity;
        }
    }
}
